# coding:utf-8
# 王一舟
# excel_tools.py
import ast
import json
import pathlib
import pandas as pd
import yaml
import math #导入Python的数学模块，用于访问数学函数和常量。

'''
导入读取excel的包pandas  -- pip install pandas 
注意: openpyxl是pandas用于读取.xlsx文件引擎之一
所以需要安装openpyxl  3.1.5 可以可以正常执行
pip install pandas openpyxl  == 3.1.5

'''


import os
from core.globalContext import g_Context



#读取环境配置文件,并把数据加到全局变量中供渲染
def load_config_excel(excel_folder_path):
    '''
        用来读取全局配置变量,把里面的数据先存放到全局变量中以供核心执行器执行每个步骤的时候一起渲染
        :param yaml_folder_path: config.xlsx配置文件所在的文件夹目录
        :return:如果文件路径不存在,返回Flase
        '''
    try:
        config_excel_path = os.path.join(excel_folder_path,'config.xlsx')  # 因为config.xlsx一般是和excel用例文件放在一个目录下的,可以直接拼接出config.xlsx的完整路径

        # TODO 1 读取excel
        df =  pd.read_excel(config_excel_path) #获取excel读取的数据

        data = {
            '_data_base': {}
        }  # 存储容器

        for index , row in df.iterrows():#遍历每一行的excel,读出来的是row = {标题名:该该标题下的值,标题名:该行该标题下的值,..}
            '''
            index:下标
            row : 每一行内容
            iterrows : 所有行的值
            '''
            #如果是变量直接存储为 变量名 : 变量值
            if row["类型"] == "变量": #判断当excel中的类型为变量时
                data[row["变量名"]] = row["变量值"] #那么直接以 变量描述 : 变量值 的形式,添加到空字典data中

            elif row["类型"] == "数据库":
                    db_name = row["变量名"]
                    # db_config = eval(row["变量值"]) #读出来的是一个字符串,eval把它变成字典类型 ,下面那个方法也可以
                    db_config = json.loads(row["变量值"]) #读出来的是一个字符串,用json.loads()把它变成字典类型

                    data['_data_base'][db_name] = db_config #把数据库的{数据库名:{数据库连接参数}}添加到_data_base中
                    #{'_data_base': {'mysql001': {'host': 'shop-xo.hctestedu.com', 'port': 3306, 'user': 'api_test', 'password': 'Aa9999!', 'db': 'shopxo_hctested'}}

        #把读取出来的excel数据存放到全局变量中,供渲染
        if data: #如果data不为空,写入全局变量
            g_Context().update_dict(data)

            print("全局变量:",g_Context().show_dict())
    except Exception as e:  # 如果路径不对报错
        print(f"传入的是:{excel_folder_path},需要传入config.xlsx所在文件夹路径,错误:{e}")
        return False



#读取excel的测试用例数据
def load_excel_files(excel_folder_path):
    '''
    返回满足条件的excel文件列表及数据
    :param excel_folder_path: excel用例存放的目录
    :return:
    '''
    # TODO 1 调用读取全局配置的方法, 因为本方法是用来读取case的,所以肯定会被调用,在读取case前先顺便把全局配置变量的数据一起加到全局变量中
    load_config_excel(excel_folder_path)  # 读取config.xlsx全局配置变量
    # TODO 2 获取满足条件的文件列表
    excel_case_infos = []  # 空列表存储所有的数据
    excel_fileName_list = [(int(f.split("_")[0]), f) for f in os.listdir(excel_folder_path) if
                      f.endswith(".xlsx") and f.split("_")[0].isdigit()]
    '''
    (f.split("_")[0],f) : 代表以元组接收结果,内容是(int(序号),符合条件的文件名), 最后excel_case_list是一个[(1,文件名),(2,文件名),(3,文件名)]的格式 ,序号提取出来是为了后续排序用
    for f in os.listdir(excel_folder_path)  :  os.listdir(目录路径) , 返回的是一个列表, 可以获取指定目录下的所有文件名
         - 然后遍历这个目录下的所有文件名
    if f.endswith(".xlsx") and f.split("_")[0].isdigit() : 将文件名以"_"分隔获取返回的列表的第0个值(即_前面的序号),判断这个序号是不是数字,如果符合这2个条件就把这个文件名添加到空列表
    输出: [('01', '01_登录成功测试用例.xlsx'), ('02', '02_登录失败测试用例.xlsx'), ('03', '03_加入购物车成功用例.xlsx')]
    '''
    # TODO 3 根据列表中的二元元组的第一个值排序,然后只保留第二个值(即文件名)
    excel_fileName_list.sort()  # 排序
    excel_case_list = [f[-1] for f in excel_fileName_list]  # 遍历排序后的列表,,以f[-1]切片取出第二个值(去掉前面排序的序号,只取xlsx文件名),组成新的列表
    #拼接文件名的路径并读取excel测试用例数据到 data
    for excel_fileName in excel_case_list:
        excel_case_path = os.path.join(excel_folder_path, excel_fileName)  # 拼接每个符合条件的excel用例文件名的完整路径
        # TODO 4 读取excel用例
        data = pd.read_excel(excel_case_path,sheet_name=0)
        data = data.where(data.notnull(),None) #TODO  这个要研究一下 !  将非空值保留,空数据用None替换
        '''
        excel文件中空格里没有东西,那么pandas读取出来的是NaN,需要把它替换成None,(python只认None)
        '''
        data = data.to_dict(orient="records") #把用例转换成字典格式显示 这部很重要!data = [{第一行字典数据},{第二行字典数据},{第三行字典}..]

        curr_test_case = None #初始化一个字典,用来存放下面遍历后的每一行的测试用例
        # TODO 5 遍历excel的每一行字典数据
        for row in data:
            #row 就是每一行的字典内容
            '''
            每个测试用例存在多个步骤(一个步骤就是一行内容),但是标题只有1个,excel读取出来后,只有第一行会存在"测试用例标题"
            所以通过判断改行是否有"测试用例标题"就能知道改行是否为该用例的第一行数据
            直到遇到下一个"测试用例标题",就从头开始
            '''

            # TODO * data.where(data.notnull(),None)不一定能够好用,所以手动判断读取的列是否日然是nan,如果是nan,手动把它变成None - 代替下面的
            for k, v in row.items(): #获取字典中的k 和 v
                if isinstance(v, float) and math.isnan(v): #当value为浮点数，并且是否是 NaN时
                    row[k] = None #将nan 变为 None
            #
            # # TODO * data.where(data.notnull(),None)不一定能够好用,所以手动判断读取的列是否日然是nan,如果是nan,手动把它变成None - 弃用
            # if isinstance(row["参数_5"], float) and math.isnan(row["参数_5"]): #检查字典中当前键的值是否为浮点数，并且是否是 NaN。
            #     row["参数_5"] = None
            # if (isinstance(row["备注"], float) and math.isnan(row["备注"])):
            #     row["备注"] = None

            # TODO 5.1 检查当前行是否包含有效的测试用例标题,有则代表该行是这个测试用例的起始位,如果当前行是起始位,就构建curr_test_case字典
            if pd.notna(row["测试用例标题"]):  #代表 如果()中的数据不是NaN(空)
                #如果curr_test_case已经构建完毕,有值
                if curr_test_case is not None:
                    excel_case_infos.append(curr_test_case) # 就把当前的测试用例加到存储所有的数据的空列表中
                    '''
                    测试用例标题一个用例只有1个且只出现在第一个row中,若当前row中出现用例标题,说明上一行的内容为上个测试用例的最后一行
                    意味着上个测试用例的所有step都已经加到steps中,组成了一个完整的用例数据,此时才将这个用例数据添加到excel_case_infos列表中;
                    同样,只有当本次循环的row中出现测试用例标题时才会执行下一步的初始化curr_test_case数据,然后再逐个补足steps中的步骤,
                    只要当前行中不出现测试用例标题,就不会初始化curr_test_case,而是在意构建的curr_test_case中的steps[]中添加step
                    '''
                #初始化一个测试用例字典
                curr_test_case = {
                "desc": row["测试用例标题"],
                "featureName": row["一级模块"],#用于生成动态报告
                "storyName": row["二级模块"],#用于生成动态报告
                "rank": row["用例等级"],#用于生成动态报告
                "remark": row["备注"],#用于生成动态报告
                "steps": []
            }
            # TODO 5.2 初始化步骤的值 step ,每个row都有不同的步骤描述和步骤内容所以每次都重置step{}
            step = {row["步骤描述"]:{
                        "关键字":row["关键字"],
                        }
                    }
            # TODO 5.3 考虑步骤中每个参数
            parameter = [] #存放步骤中参数的列表 ,如[参数_1的value,参数_2的value,参数_3的value,...]
            for key, value in row.items(): #如果key中包含 "参数_",那说明它的值是参数内容,全部放到空列表中
                if "参数_" in key:
                    '''
                    ast.literal_eval(),功能类似eval(),将字符串转换为有效的表达式参与求值运算返回计算结果
                    区别:
                    ast.literal_eval()会判断需要计算的内容计算后是不是合法的Python类型，如果是则进行运算，否则就不进行运算。
                    eval()风险更高 ,所以出于安全考虑，对字符串进行类型转换的时候，最好使用ast.literal_eval()
                    '''
                    try: #这里加上异常捕捉是因为ast.literal_eval()不能转换url中的连接(会报错),所以它能转就转不能转就pass,直接添加
                        value = ast.literal_eval(value)  # 尝试将字符串转换为有效的python表达式
                    except:
                        pass
                    parameter.append(value)
            # TODO 5.4 把步骤参数的值和rquest请求的key一一对应 -- zip()
            # TODO 5.4 读取keywords.yaml的数据,用与构建step-步骤参数的值和rquest请求的key一一对应
            excel_keywords = {}
            file = pathlib.Path(__file__).parents[1].resolve() / 'extend' / 'excel_keywords.yaml'
            with open(file, "r", encoding="utf-8") as f:
                excel_keywords = yaml.full_load(f)  # 格式 {"关键字":['URL', 'PARAMS', 'HEADERS', 'DATA'],"关键字":['EXVALUE', 'INDEX', 'VARNAME'],...}
            # 列表推导式 dict_paramete = {k:v for k,v in zip(key,value)}
            dict_paramete = {k:v for k,v in zip(excel_keywords[row["关键字"]],parameter)}

            #通过zip()遍历excel_keywords中的步骤参数名[],以及parameter这2个列表,把excel每一行的参数合并组成一个字典

            #组成完整的步骤数据
            step[row["步骤描述"]].update(dict_paramete)
            #把完整的步骤数据添加到当前测试用例中,构成完整的测试用例数据
            curr_test_case["steps"].append(step)

        if curr_test_case is not None: #如果当前case不是None, 就把当前case 加到所有用例的列表中
            excel_case_infos.append(curr_test_case)
            '''
            这里的curr_test_case是所有row读取完之后构建的最后一个用例的数据,因为最后一行读完了就不会再进入for row in data循环,
            那么最后一个curr_test_case用例数据无法添加到excel_case_infos中,所以这里手动判断再添加
            '''
    return excel_case_infos



def excel_case_parser(excel_folder_path):
    '''
        核心执行器不仅可以执行yaml,还要能执行excel
        所以要统一一个规范的格式进行处理
        返回指定的格式
        :param excel_folder_path: 传入excel文件存放的文件夹路径
        :return: {
                "case_infos": case_infos,  #  所有的测试用例 []
                "case_names": case_names   #  所有测试用例对应的标题 []
                }
        '''
    # TODO 1 :对应固定的格式
    case_infos = []  # 存放用例数据
    case_names = []  # 存放用例名称
    #TODO 2 :调用对应的方法,拿到所有的excel用例数据
    excel_case_infos = load_excel_files(excel_folder_path)

    # TODO 3 :统一处理成一样的格式(后面excel也需要同样处理)
    for info in excel_case_infos: #遍历excel读取的用例数据

        info.update({"_case_name":info["desc"]})
        case_infos.append(info)  # 所有的测试用例[]
        case_names.append(info["desc"])  # 所有的用例名称[]

    data = {
        "case_infos": case_infos,  # 所有的测试用例 []
        "case_names": case_names  # 所有测试用例对应的标题 []
    }

    return data





if __name__ == '__main__':
    excel_folder_path = pathlib.Path(__file__).parents[1].resolve() / 'excel_files'
    data = excel_case_parser(excel_folder_path)
    print(data)

